import backtrader as bt
from backtrader.utils.datehelper import date2str, timestamp2str, timestamp2date
from datetime import datetime, timedelta
from backtrader.brokers.ccxtorder import CCXTOrder, Okex5CCXTOrder


class BrokerProxy:
    def __init__(self, strat):
        self.strat = strat

    def parse_close_trader(self, order):
        pass

'''
如果有新的交易所加进来, 必须这样命名
'''


class BinanceBrokerProxy(BrokerProxy):
    def __init__(self, strat):
        super(BinanceBrokerProxy, self).__init__(strat)
    """
        买现货, 现货market买入的size是买多少钱
    """
    def buy_spot(self, data, exectype, size=None, price=None):
        if not self.strat.allow_trade():
            content = "达到交易上限[%s > %s]" % (self.strat.trade_count, self.strat.p.max_trade)
            self.strat.send_msg(data._name, "触发交易上限", content, date2str(datetime.today()))
            raise Exception(f"System auto exit")

        if size is None or price is None:
            raise Exception(f"market and limit must give the price and size, the system need trace.")

        if self.strat.p.debug:
            cash, value = self.strat.broker.getcash(), self.strat.broker.getvalue(self.strat.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("BUY SPOT", data._name, text, bar_dt0)

        trigger_px = price
        if exectype == bt.Order.Limit and self.strat.p.slippage > 0:
            price = price * (1 + self.strat.p.slippage)
        self.strat.trade_count += 1

        # price统一指定, 但是币安最后是按照size指定买卖
        if exectype == bt.Order.Market:
            price = None
        return self.strat.buy(data, exectype=exectype, size=size, price=price, plimit=trigger_px)

    """
    卖现货
    """

    def sell_spot(self, data, exectype, size=None, price=None):
        if size is None or price is None:
            raise Exception(f"market and limit must give the price and size, the system need trace.")

        if self.strat.p.debug:
            cash, value = self.strat.broker.getcash(), self.strat.broker.getvalue(self.strat.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("SELL SPOT", data._name, text, bar_dt0)
        trigger_px = price
        if exectype == bt.Order.Limit and self.strat.p.slippage > 0:
            price = price * (1 - self.strat.p.slippage)

        # price统一指定, 但是币安最后是按照size指定买卖
        if exectype == bt.Order.Market:
            price = None
        return self.strat.sell(data, exectype=exectype, size=size, price=price, plimit=trigger_px)

    """
        止损单
        STOP_LOSS	quantity, stopPrice
        STOP_LOSS_LIMIT quantity, stopPrice, price
        币安交易所不支持stop_loss
        """

    def stop_loss_spot(self, data, exectype, size, price, trigger_px):
        params = {
            "type": "spot",
            "stopPrice": trigger_px
        }
        if size is None or price is None or trigger_px is None:
            raise Exception(f"stop limit must give the price and size, the system need trace.")

        if exectype not in [bt.Order.StopLimit]:
            raise Exception("exec type is not Stop or StopLimit.")
        if exectype == bt.Order.StopLimit and price is None:
            raise Exception("exec type StopLimit, price is must given.")
        if exectype is None or size is None or trigger_px is None or price is None:
            raise Exception("the parameter is not given.")

        if self.strat.p.debug:
            cash, value = self.strat.broker.getcash(), self.strat.broker.getvalue(self.strat.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Stop Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, trigger_px if trigger_px else 0,
                      money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("STOPLOSS SPOT", data._name, text, bar_dt0)
        return self.strat.sell(data, exectype=exectype, size=size, price=price, plimit=trigger_px, params=params)

    """
    市价止盈单
    STOP_LOSS_LIMIT	timeInForce, quantity, price, stopPrice
    """

    def take_profit_spot(self, symbol, size, stop_price):
        params = {
            "type": "spot",
            "stopPrice": stop_price
        }
        try:
            exchange = self.strat.broker.store.exchange
            _order = exchange.create_order(symbol=symbol, type="TAKE_PROFIT", side="SELL", amount=size, price=None,
                                           params=params)
            return _order["id"]
        except Exception as e:
            self.strat.log(e)
        return None

    """
    限价止盈单
    STOP_LOSS_LIMIT	timeInForce, quantity, price, stopPrice
    """

    def take_profit_limit_spot(self, symbol, size, stop_price, price):
        params = {
            "type": "spot",
            "stopPrice": stop_price
        }
        try:
            exchange = self.strat.broker.store.exchange
            _order = exchange.create_order(symbol=symbol, type="TAKE_PROFIT_LIMIT", side="SELL", amount=size,
                                           price=price, params=params)
            return _order["id"]
        except Exception as e:
            self.strat.log(e)
        return None

    """
   exectype[type]: LIMIT, MARKET, STOP, TAKE_PROFIT, STOP_MARKET, TAKE_PROFIT_MARKET,TRAILING_STOP_MARKET
   """

    def buy_future(self, data, exectype, size, order_price=None):
        if not self.strat.allow_trade():
            content = "达到交易上限[%s > %s]" % (self.strat.trade_count, self.strat.p.max_trade)
            self.strat.send_msg(data._name, "触发交易上限", content, date2str(datetime.today()))
            raise Exception(f"System auto exit")

        if size is None or order_price is None:
            raise Exception(f"market and limit must give the price and size, the system need trace.")

        if self.strat.p.debug:
            cash, margin = self.strat.broker.getcash(), self.strat.broker.getmarginvalue()
            money = self.strat.broker.getvalue()

            text = "Cash: %.2f, Margin: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, margin, bt.Order.ExecTypes[exectype], order_price if order_price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("BUY Future", data._name, text, bar_dt0)

        params = {
            "type": "future",
            "positionSide": "BOTH"
        }

        trigger_px = order_price
        if exectype == bt.Order.Limit and self.strat.p.slippage > 0:
            price = order_price * (1 + self.strat.p.slippage)
        self.strat.trade_count += 1

        # price统一指定, 但是币安最后是按照size指定买卖
        if exectype == bt.Order.Market:
            order_price = None
        return self.strat.buy(data, exectype=exectype, size=size, price=order_price, plimit=trigger_px, params=params)
    """
    合约平仓
    """
    def sell_future(self, data, exectype, size, order_price=None):
        if size is None or order_price is None:
            raise Exception(f"market and limit must give the price and size, the system need trace.")

        if self.strat.p.debug:
            cash, margin = self.strat.broker.getcash(), self.strat.broker.getmarginvalue()
            money = self.strat.broker.getvalue()

            text = "Cash: %.2f, Margin: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, margin, bt.Order.ExecTypes[exectype], order_price if order_price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("SELL SPOT", data._name, text, bar_dt0)

        params = {
            "type": "future",
            "positionSide": "BOTH"
        }
        trigger_px = order_price
        if exectype == bt.Order.Limit and self.strat.p.slippage > 0:
            price = order_price * (1 - self.strat.p.slippage)

        # price统一指定, 但是币安最后是按照size指定买卖
        if exectype == bt.Order.Market:
            order_price = None
        return self.strat.sell(data, exectype=exectype, size=size, price=order_price, plimit=trigger_px, params=params)

    """
    止盈止损单设置
    """

    def stop_profit_future(self, data, exectype, side, size, order_price, trigger_px):
        """
        @side: buy or sell
        @order_price: 委托价格, stop or take_profit 起作用
        @trigger_px: 触发价格
        """
        if exectype not in [bt.Order.StopLimit, bt.Order.Stop, bt.Order.Profit, bt.Order.ProfitLimit]:
            raise Exception("exec type is not Stop or StopLimit.")

        if exectype in [bt.Order.StopLimit, bt.Order.ProfitLimit] and order_price is None:
            raise Exception(f"stop profit limit must give the price and size, the system need trace.")

        if trigger_px is None:
            raise Exception(f"stop profit limit must give the trigger price.")

        if size is None:
            raise Exception(f"stop profit limit must give the size.")

        if side is None:
            raise Exception(f"stop profit limit must give the side, buy or sell?")

        if self.strat.p.debug:
            cash, margin = self.strat.broker.getcash(), self.strat.broker.getmarginvalue()
            money = self.strat.broker.getvalue()

            text = "Cash: %.2f, Margin: %.2f, Type: %s, Price: %.6f, Stop Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, margin, bt.Order.ExecTypes[exectype], order_price if order_price else 0, trigger_px if trigger_px else 0,
                      money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("STOPLOSS FUTURE", data._name, text, bar_dt0)

        # 市价止盈和止损不需要指定委托价格
        if exectype == bt.Order.Stop or exectype == bt.Order.Profit:
            order_price = None

        params = {
            "type": "future",
            "stopPrice": trigger_px,
            "workingType": "MARK_PRICE",
            "priceProtect": "TRUE",
            "reduceOnly": "true"
        }
        if side == "buy":
            return self.strat.buy(data, exectype=exectype, size=size, price=order_price, plimit=trigger_px, params=params)
        else:
            return self.strat.sell(data, exectype=exectype, size=size, price=order_price, plimit=trigger_px, params=params)

    #  header = "strat,account,symbol,bar_time,order_time,trade_time,id,side,
    #   type,trigger_px,order_price,order_size,trade_price,trade_size,comm\n"
    def parse_close_trader(self, order):
        symbol = order.data._name
        trader = [
            date2str(bt.num2date(order.created.dt)),
            date2str(timestamp2date(order.order_time / 1000) + timedelta(hours=12)),     # 委托时间
            date2str(timestamp2date(order.trade_time / 1000) + timedelta(hours=12)),     # 成交时间
            order.id,
            order.ordtypename(),        # buy or sell
            order.getordername(),       # market or limit
            order.plimit,               # 信号价
            order.price,                # 委托价
            order.size,                 # 委托大小
            order.executed.price,       # 成交价
            order.executed.size,        # 成交大小
            order.executed.comm         # 无用
        ]
        return trader
        # return list(map(lambda x: "" if x is None else x, trader))

'''
如果有新的交易所加进来, 必须这样命名
'''
class Okex5BrokerProxy(BrokerProxy):
    def __init__(self, strat):
        super(Okex5BrokerProxy, self).__init__(strat)
    """
        买现货, 现货market买入的size是买多少钱
    """
    def buy_spot(self, data, exectype, size=None, price=None):
        if not self.strat.allow_trade():
            content = "达到交易上限[%s > %s]" % (self.strat.trade_count, self.strat.p.max_trade)
            self.strat.send_msg(data._name, "触发交易上限", content, date2str(datetime.today()))
            raise Exception(f"System auto exit")

        # ok很奇怪, market的时候, size代表的不是数量而是金额, 这里我统一处理, 指定size和price, 计算成金额
        if size is None or price is None:
            raise Exception(f"market and limit must give the price and size, the system need trace.")

        if self.strat.p.debug:
            cash, value = self.strat.broker.getcash(), self.strat.broker.getvalue(self.strat.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("BUY SPOT", data._name, text, bar_dt0)

        trigger_px = price
        if exectype == bt.Order.Limit and self.strat.p.slippage > 0:
            price = price * (1 + self.strat.p.slippage)

        self.strat.trade_count += 1
        return self.strat.buy(data, exectype=exectype, size=size, price=price, plimit=trigger_px, tdMode="cash")
    """
    卖现货
    """

    def sell_spot(self, data, exectype, size=None, price=None):
        if size is None or price is None:
            raise Exception(f"market and limit must give the price and size, the system need trace.")

        if self.strat.p.debug:
            cash, value = self.strat.broker.getcash(), self.strat.broker.getvalue(self.strat.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("SELL SPOT", data._name, text, bar_dt0)

        trigger_px = price
        if exectype == bt.Order.Limit and self.strat.p.slippage > 0:
            price = price * (1 - self.strat.p.slippage)
        return self.strat.sell(data, exectype=exectype, size=size, price=price, plimit=trigger_px, tdMode="cash")

    """
        止损单
        STOP_LOSS	quantity, stopPrice
        STOP_LOSS_LIMIT quantity, stopPrice, price
        币安交易所不支持stop_loss
        """

    def stop_loss_spot(self, data, exectype, size, price, trigger_px):
        if size is None or price is None or trigger_px is None:
            raise Exception(f"stop limit must give the price and size, the system need trace.")

        if exectype not in [bt.Order.StopLimit]:
            raise Exception("exec type is not Stop or StopLimit.")
        if exectype == bt.Order.StopLimit and price is None:
            raise Exception("exec type StopLimit, price is must given.")
        if exectype is None or size is None or trigger_px is None or price is None:
            raise Exception("the parameter is not given.")

        if self.strat.p.debug:
            cash, value = self.strat.broker.getcash(), self.strat.broker.getvalue(self.strat.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Stop Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, trigger_px if trigger_px else 0,
                      money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.strat.log("STOPLOSS SPOT", data._name, text, bar_dt0)

        return self.strat.sell(data, exectype=exectype, size=size, price=price, plimit=trigger_px, tdMode="cash",
                               loss_price=price, trigger_loss=trigger_px)

    """
    市价止盈单
    STOP_LOSS_LIMIT	timeInForce, quantity, price, stopPrice
    """

    def take_profit_spot(self, symbol, size, stop_price):
        params = {
            "type": "spot",
            "stopPrice": stop_price
        }
        try:
            exchange = self.strat.broker.store.exchange
            _order = exchange.create_order(symbol=symbol, type="TAKE_PROFIT", side="SELL", amount=size, price=None,
                                           params=params)
            return _order["id"]
        except Exception as e:
            self.strat.log(e)
        return None

    """
    限价止盈单
    STOP_LOSS_LIMIT	timeInForce, quantity, price, stopPrice
    """

    def take_profit_limit_spot(self, symbol, size, stop_price, price):
        params = {
            "type": "spot",
            "stopPrice": stop_price
        }
        try:
            exchange = self.strat.broker.store.exchange
            _order = exchange.create_order(symbol=symbol, type="TAKE_PROFIT_LIMIT", side="SELL", amount=size,
                                           price=price, params=params)
            return _order["id"]
        except Exception as e:
            self.strat.log(e)
        return None

    #  header = "strat,account,symbol,bar_time,order_time,trade_time,id,side,
    #   type,trigger_px,order_price,order_size,trade_price,trade_size,comm\n"
    def parse_close_trader(self, order):
        symbol = order.data._name
        if isinstance(order, Okex5CCXTOrder):
            trader = [
                date2str(bt.num2date(order.created.dt)),
                timestamp2str(order.order_time / 1000),
                timestamp2str(order.ref_order.trade_time / 1000),
                order.id,
                order.ordtypename(),
                order.getordername(),
                order.plimit,
                order.price,
                order.size,
                order.ref_order.executed.price,
                order.ref_order.executed.size,
                order.ref_order.executed.comm
            ]
        else:
            trader = [
                date2str(bt.num2date(order.created.dt)),
                timestamp2str(order.order_time / 1000),
                timestamp2str(order.trade_time / 1000),
                order.id,
                order.ordtypename(),
                order.getordername(),
                order.plimit,
                order.price,
                order.size,
                order.executed.price,
                order.executed.size,
                order.executed.comm
            ]
        return list(map(lambda x: "" if x is None else x, trader))